#region Licence
// Copyright (c) 2008 Tasman Logic Ltd

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
// and associated documentation files (the "Software"), to deal in the Software without restriction, 
// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
// subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies 
// or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

using ActiveSharp.Inspection;

namespace ActiveSharp.PropertyMapping
{
    /// <summary>
    /// Given a field name, returns the property which uses it
    /// </summary>
    /// <remarks>This code only makes the link between field and property if the property has a setter method,
    /// and if that setter method passes the field, by reference, to some other method. (Typically a method called SetValue,
    /// if you are following the conventions suggested in the documentation for this library, but the name doesn't actually matter to
    /// <see cref="FieldToPropertyMap"/>)</remarks>
    public class FieldToPropertyMap
    {
        public FieldToPropertyMap(Type targetType)
        {
            PopulateMap(targetType);
        }

        private readonly Dictionary<string, PropertyInfo> _map = new Dictionary<string, PropertyInfo>();

        /// <summary>
        /// Returns the matching property
        /// </summary>
        /// <param name="fieldName">Note that this accepts strings, not <see cref="FieldInfo"/>s because
        /// <see cref="OffsetToFieldMap"/> outputs strings</param>
        public PropertyInfo this[string fieldName]
        {
            get { return _map[fieldName]; }
        }

        internal PropertyInfo GetPropertyInfoOrNull(string fieldName)
        {
            PropertyInfo result;
            if (_map.TryGetValue(fieldName, out result))
                return result;
            else
                return null;
        }

        /// <summary>
        /// Populates the mapping
        /// </summary>
        private void PopulateMap(Type targetType)
        {
            foreach (PropertyInfo property in targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                )
            {
                FieldInfo foundField = null;

                // look for call(s) to ldflda, which load fields for passing byref
                MethodInspector inspector = new MethodInspector(property.GetSetMethod(true) ?? property.GetGetMethod(true));  // might use this with get method only in some rare cases (e.g. lazy load of a read only property in an ORM situation)
                foreach (Instruction current in inspector.Instructions)
                {
                    if (current.OpCode == OpCodes.Ldflda && // load a field for use "by ref"
                        current.Offset(-1).OpCode == OpCodes.Ldarg_0) // and the field belongs to "this" (which is always arg0)
                    {
                        FieldInfo currentField = current.Operand.AsField;

                        // weed out those cases where the ldflda is just so that we can call a method on a struct field
                        if (IsLoadForCallOnStruct(current, currentField.FieldType))
                            continue; ;

                        if (foundField == null)
                        {
                            foundField = currentField;
                        }
                        else if (foundField != currentField)
                        {
                            throw new SetterCodingConventionException(
                                "A single property may only pass one field as a 'by ref' parameter.  If you need to do a second one, copy it's value to a local variable and pass that by ref, but note that you can't use the local variable with " +
                                    typeof(PropertyMap) + ". See property " + property + " on class " + property.DeclaringType);
                        }
                    }
                }

                // record the mapping if we found one.
                // Don't complain if we didn't.  (May write other code, elsewhere, to verify that all properties are written to the desired coding convention)
                if (foundField != null)
                {
                    if (_map.ContainsKey(foundField.Name))
                    {
                        throw new NotSupportedException("Field is passed 'by ref' by multiple properties.  This is not supported by ActiveSharp's parsing algorithm.  Field " +
                            foundField.Name + " is used by " + _map[foundField.Name].Name + " and " + property.Name);
                    }
                    _map.Add(foundField.Name, property);
                }
            }
        }

        /// <summary>
        /// Is the ldflda call done in order to call a method on a value-type field
        /// </summary>
        bool IsLoadForCallOnStruct(Instruction ldfldaInstruction, Type fieldType)
        {
            if (fieldType.IsValueType)
            {
                Instruction next = ldfldaInstruction.Offset(1);

                if (next.OpCode == OpCodes.Call || next.OpCode == OpCodes.Callvirt)
                {
                    // a call to a non-virtual method on the struct. (Don't think we'll ever see this code path execute with the Callvirt opcode, expect all virtual calls to use the constrained prefix, below)
                    MethodBase method = next.Operand.AsMethod;
                    bool isCallOnFieldType = method.DeclaringType == fieldType;
                    return isCallOnFieldType;
                }
                else if(next.OpCode == OpCodes.Constrained)  // the "constrained." prefix (to a callvirt that immediately follows it)
                {
                    // A call to a virtual method on the struct
                    // E.g. a method inherited from Object, and not overridden by the struct
                    // Expect compiler to emit this prefix for all such cases (see doco on the constrained prefix)
                    Type typeWhichCallIsConstrainedTo = next.Operand.AsType;
                    bool isCallOnFieldType = typeWhichCallIsConstrainedTo == fieldType;
                    return isCallOnFieldType;
                }
            }
            return false;
        }
    }
}
